/*
 * Copyright © 2017-2023 TemplateFactory(xiaoymin@foxmail.com)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */


package com.github.xiaoymin.template.internel;

import org.apache.metamodel.factory.DataContextFactory;

import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.ServiceLoader;

/**
 * This class creates the different objects needed by the generator.
 *
 * @author Jeff Butler
 */
public class ObjectFactory {
    
    private static List<ClassLoader> externalClassLoaders;
    
    static {
        externalClassLoaders = new ArrayList<>();
    }
    
    /**
     * Utility class. No instances allowed.
     */
    private ObjectFactory() {
        super();
    }
    
    /**
     * Clears the class loaders.  This method should be called at the beginning of
     * a generation run so that and change to the classloading configuration
     * will be reflected.  For example, if the eclipse launcher changes configuration
     * it might not be updated if eclipse hasn't been restarted.
     * 
     */
    public static void reset() {
        externalClassLoaders.clear();
    }
    
    /**
     * Adds a custom classloader to the collection of classloaders searched for "external" classes. These are classes
     * that do not depend on any of the generator's classes or interfaces. Examples are JDBC drivers, root classes, root
     * interfaces, etc.
     *
     * @param classLoader
     *            the class loader
     */
    public static synchronized void addExternalClassLoader(
                                                           ClassLoader classLoader) {
        ObjectFactory.externalClassLoaders.add(classLoader);
    }
    
    public static ServiceLoader<DataContextFactory> externalClassLoaderContextFactory() {
        ServiceLoader<DataContextFactory> serviceLoader = null;
        for (ClassLoader classLoader : externalClassLoaders) {
            try {
                serviceLoader = ServiceLoader.load(DataContextFactory.class, classLoader);
                if (serviceLoader != null) {
                    break;
                }
            } catch (Exception e) {
                // ignore - fail safe below
            }
        }
        return serviceLoader;
    }
    
    /**
     * Returns a class loaded from the context classloader, or the classloader supplied by a client. This is
     * appropriate for JDBC drivers, model root classes, etc. It is not appropriate for any class that extends one of
     * the supplied classes or interfaces.
     *
     * @param type
     *            the type
     * @return the Class loaded from the external classloader
     * @throws ClassNotFoundException
     *             the class not found exception
     */
    public static Class<?> externalClassForName(String type) throws ClassNotFoundException {
        
        Class<?> clazz;
        
        for (ClassLoader classLoader : externalClassLoaders) {
            try {
                clazz = Class.forName(type, true, classLoader);
                return clazz;
            } catch (Exception e) {
                // ignore - fail safe below
            }
        }
        
        return internalClassForName(type);
    }
    
    public static Object createExternalObject(String type) {
        Object answer;
        
        try {
            Class<?> clazz = externalClassForName(type);
            answer = clazz.getConstructor().newInstance();
        } catch (Exception e) {
            throw new RuntimeException(e.getMessage());
        }
        
        return answer;
    }
    
    public static Class<?> internalClassForName(String type) throws ClassNotFoundException {
        Class<?> clazz = null;
        
        try {
            ClassLoader cl = Thread.currentThread().getContextClassLoader();
            clazz = Class.forName(type, true, cl);
        } catch (Exception e) {
            // ignore - failsafe below
        }
        
        if (clazz == null) {
            clazz = Class.forName(type, true, ObjectFactory.class.getClassLoader());
        }
        
        return clazz;
    }
    
    public static URL getResource(String resource) {
        URL url;
        
        for (ClassLoader classLoader : externalClassLoaders) {
            url = classLoader.getResource(resource);
            if (url != null) {
                return url;
            }
        }
        
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        url = cl.getResource(resource);
        
        if (url == null) {
            url = ObjectFactory.class.getClassLoader().getResource(resource);
        }
        
        return url;
    }
    
    public static Object createInternalObject(String type) {
        Object answer;
        
        try {
            Class<?> clazz = internalClassForName(type);
            
            answer = clazz.getConstructor().newInstance();
        } catch (Exception e) {
            throw new RuntimeException(e.getMessage());
            
        }
        
        return answer;
    }
    
}
